Racket 语言宣言:用语言创造语言的设计哲学

#Innolight #Lisp #Racket

引言

Racket 是一门独特的编程语言,它不仅仅是用来编写程序的工具,更是一门"用来创造编程语言的编程语言"。经过 20 年的发展,Racket 的设计团队提炼出了三条核心设计原则,这些原则塑造了 Racket 的独特面貌。

Racket 的起源

1995 年,Racket 项目最初是为初学者设计的教学工具。团队发现,尽管 Scheme 语言简洁优雅,但在一小时内教会学生 Scheme 并让他们专注于计算本质是不现实的。他们需要的是一门专门为初学者设计的教学语言,以及一个友好的编程环境,而不是像 Emacs 或 Vi 这样让新手望而生畏的专业工具。

虽然 Scheme 是一个很好的起点,但团队很快发现需要更多功能。他们需要比 Lisp 传统宏系统更强大的语法扩展机制,需要真正的安全抽象而非简单的 lambda 模拟,还需要在自己的程序控制下执行任意程序的能力。

到 2010 年,这门语言已经发展得如此不同,团队将其重命名为 Racket,向世界宣告这是一门全新的语言。

三大设计原则

原则一:快速创建新编程语言

编程是一种问题解决活动。优秀的程序员应该用问题领域的语言来陈述问题和表达解决方案。因此,Racket 的首要目标就是帮助程序员快速创建和部署新语言。

更重要的是,创建和部署语言的机制必须包含在语言本身之中——一旦安装了 Racket,就无需借助外部工具来使用新语言。这与许多使用外部工具和命令行预处理器来创建领域特定语言的做法形成鲜明对比。

实例说明

#lang racket
(provide walk-simplex)

(require "small.sim" 2htdp/image)

(define (walk-simplex timing)
  ... (maximizer #:x 2) ...)

上面的模块使用标准的 racket 语言。但如果问题更适合用特定领域语言表达,你可以创建自己的语言:

#lang simplex
;; 用线性不等式定义单纯形
#:variables x y
3 * x + 5 * y <= 10
3 * x - 5 * y <= 20

Racket 的语法扩展系统不仅能扩展现有语言,更重要的是能定义全新的语言。例如,Racket 的类系统、组件系统和循环推导式都是作为子语言实现的,但它们看起来就像 Racket 的核心特性一样自然。

原则二:全方位的保护机制

如果编程是用正确的语言解决问题,那么系统必然由多个不同语言编写的互联组件构成。由于组件间的连接,数据会在不同语言上下文间流动。既然语言负责提供和维护不变量,语言创建者就必须有能力保护这些不变量。

Racket 提供了构建保护机制的基础模块,支持从 C 语言级别的位操作到类型安全扩展的完整语言谱系。

语言谱系的四个层次

  1. FFI 包装器:直接与 C 库交互,实现高性能但较低安全性的代码
  2. 纯 Racket 代码:获得操作安全和内存安全
  3. 带契约的代码:通过契约保护组件间的交互
  4. 显式类型:提供静态类型检查,提高性能和可维护性
#lang typed/racket
(provide walk-simplex)

(: walk-simplex (-> Natural Video))

(define (walk-simplex timing)
  ... (maximizer #:x 2) ...)

Racket 的契约系统基于代理机制实现,能够处理从简单值到复杂的高阶函数、对象和类的保护。类型化的模块会将导出值的类型翻译为运行时契约,这确保了一种通用的类型安全性。

原则三:将外部机制转化为语言构造

当程序员必须求助于语言外的机制来解决问题时,说明这门语言辜负了他们。传统 IDE 依赖操作系统机制来管理项目、执行程序和检查状态,而 Racket 将这些外部机制内化为语言构造。

检查器(Inspectors)

Racket 在检查器层次结构下执行模块。如果两个模块在同一检查器或不可比较的检查器下运行,它们无法查看彼此的结构——除非显式授权。但如果模块 A 在检查器 i 下运行,而模块 B 在 i 的下级检查器 j 下运行,A 就可以检查 B 的结构,无论 B 是否授权。

这使得 DrRacket IDE 能够在执行学生程序时访问结构信息进行打印、单步调试等操作,同时保持正常的模块封装性。

资源管理器(Custodians)

(define cc (current-custodian))
(define c* (make-custodian))
(parameterize ([current-custodian c*])
  ;; 在新 custodian 下分配资源
  ...)
;; 出错时关闭所有资源
(when (exn? x)
  (custodian-shutdown-all c*)
  (raise x))

这种机制让程序能够精细控制文件句柄、套接字和数据库连接等资源,而不需要依赖操作系统的进程管理。

设计反馈循环

Racket 的设计不是闭门造车,而是在一个包含多个维度的反馈循环中不断演进:

Redex 是 Racket 语法扩展系统最复杂的客户,它在两个层面使用语法扩展:编译 Redex 语言的语法定义,以及作为编译目标。Scribble 是用于创建 Racket 文档的领域特定语言,它能引用和计算库中的绑定,从而实现深度交叉引用。

这些应用既验证了 Racket 的设计原则,也暴露了不足,推动着语言的持续改进。

未来挑战与研究方向

Racket 团队坦诚地指出了当前的不足之处,并将其视为未来研究的机会:

  1. 学习曲线陡峭:语法扩展系统功能强大但难以掌握,需要简化和更好的文档
  2. 关注点分离:当前无法像传统语言那样分离规约与实现,需要更多研究
  3. IDE 自动化:新语言应该能自动派生出 IDE 支持,这方面还需大量工作
  4. 类型系统增强:向依赖类型方向发展,提供更强的静态保证
  5. 安全性改进:需要更系统地处理安全策略和执行

结语

Racket 的三大设计原则——语言创造、全谱保护、机制内化——不仅塑造了一门独特的编程语言,更提出了一种编程哲学:用恰当的语言解决问题,而不是迁就现有语言的限制

经过 20 年的发展,Racket 已经从一个教学项目成长为一个成熟的语言工程平台。它证明了:当我们认真对待"语言"这个概念,将其作为解决问题的首要工具时,编程可以变得更加优雅和强大。

虽然仍有许多不完善之处,但 Racket 团队将这些视为前进的动力。正如他们所说,将原则变为现实总会产生不完美的产物,而这些不完美正是未来研究的机会。

![[The Racket Manifesto.pdf]]